package user

import (
	"bytes"
	"errors"
	"gitee.com/yysf-xin/dproxy/pkg/app"
	"gitee.com/yysf-xin/dproxy/pkg/db"
	"gitee.com/yysf-xin/dproxy/pkg/model"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/jwtauth/v5"
	"github.com/lestrrat-go/jwx/jwt"
	"golang.org/x/crypto/bcrypt"
	"net/http"
	"strconv"
	"strings"
	"time"
)

type loginFrom struct {
	Password string
	Type     string
	Username string
}

type User struct {
	model.Model
	model.Enable
	Username string `json:"username" gorm:"uniqueIndex;size:80"`
	Password string `json:"password" gorm:"size:80"`
	Avatar   string `json:"avatar" gorm:"size:128"`
	RoleId   uint   `json:"-"`
	Role     *Role  `json:"role" gorm:"-"`
}

type Role struct {
	model.Model
	Name  string `json:"name" gorm:"size:80"`
	Value string `json:"value" gorm:"size:80"`
}

type modelDB struct {
	User *User
	Role *Role
}
type Control struct {
	DB    *db.Client
	model modelDB
	auth  *jwtauth.JWTAuth
}

func NewJwtAuthToken() *jwtauth.JWTAuth {
	return jwtauth.New("HS256", []byte("#@1!2AjKLi@"), nil)
}
func NewUserControl(DB *db.Client, config *app.Config, auth *jwtauth.JWTAuth) (u *Control, err error) {
	u = &Control{
		DB:   DB,
		auth: auth,
	}
	if config.File.Db.Upgrade {
		err = db.UpgradeDB(&u.model, DB.Db)
		if err == nil {
			var count int64
			err = DB.Db.Model(u.model.User).Count(&count).Error
			if err == nil && count == 0 {
				err = u.initUserDB()
			}
		}
	}
	if err != nil {
		u = nil
	}
	return
}
func HashPassword(password string) (string, error) {
	buf, err := bcrypt.GenerateFromPassword([]byte(password), 14)
	return string(buf), err
}
func CheckPasswordHash(password, hash string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
	return err == nil
}

func (u *Control) initUserDB() (err error) {
	s := "管理员,admin;普通用户,user"
	for _, ru := range strings.Split(s, ";") {
		rr := strings.Split(ru, ",")
		role := &Role{
			Name:  rr[0],
			Value: rr[1],
		}
		err = u.DB.Db.Model(u.model.Role).Save(role).Error
		if err != nil {
			return
		}
	}
	if err == nil {
		var (
			user = User{
				Username: "admin",
				RoleId:   1,
			}
		)
		err = u.DB.Db.Model(u.model.User).Save(&user).Error
	}
	return
}

var (
	UserNotFound = errors.New("用户不存在")
	UserPwdError = errors.New("密码错误")
	RoleNotFound = errors.New("权限不存在")
	TokenError   = errors.New("解析token错误")
)

type Token struct {
	Uid, Rid uint
	Name     string
}
type CurrentUser struct {
	Name        string `json:"name"`
	Userid      uint   `json:"userid"`
	NotifyCount int    `json:"notifyCount"`
	UnreadCount int    `json:"unreadCount"`
	Access      string `json:"access"`
}

func (u *Control) userLogin(w http.ResponseWriter, r *http.Request, form interface{}) (interface{}, error) {
	f := form.(*loginFrom)
	var user User
	if u.DB.Db.Model(u.model.User).First(&user, "username = ?", f.Username).Error != nil {
		return nil, UserNotFound
	}
	if user.Password != "" && !CheckPasswordHash(f.Password, user.Password) {
		return nil, UserPwdError
	}

	user.Role = &Role{
		Model: model.Model{
			Id: user.RoleId,
		},
	}
	err := u.DB.Db.Model(u.model.Role).First(user.Role, user.RoleId).Error

	if err != nil {
		return nil, RoleNotFound
	}

	var buf bytes.Buffer
	buf.WriteString(strconv.Itoa(int(user.Id)))
	buf.WriteString(",")
	buf.WriteString(strconv.Itoa(int(user.RoleId)))
	buf.WriteString(",")
	buf.WriteString(user.Username)

	val := map[string]interface{}{
		"usr": buf.String(),
	}
	_, tokenVal, err := u.auth.Encode(val)

	if err != nil {
		return nil, err
	}

	c := http.Cookie{
		Name:     "jwt",
		Value:    tokenVal,
		MaxAge:   60 * 60 * 24,
		Path:     "/",
		HttpOnly: true,
		SameSite: 1,
	}
	http.SetCookie(w, &c)

	return nil, nil
}

func (u *Control) logout(w http.ResponseWriter, _ *http.Request, _ interface{}) (interface{}, error) {
	c := &http.Cookie{
		Name:     "jwt",
		Path:     "/",
		Expires:  time.Unix(0, 0),
		MaxAge:   0,
		HttpOnly: true,
	}
	http.SetCookie(w, c)
	return nil, nil
}

func (u *Control) InitLogin(router chi.Router) {
	router.Route("/login/", func(r chi.Router) {
		r.Post("/account", app.JsonHttp(func() interface{} {
			return &loginFrom{}
		}, u.userLogin))
		r.Post("/logout", app.JsonHttp(nil, u.logout))
	})
}

type ModifyPwd struct {
	Password    string `json:"password"`
	OldPassword string `json:"old_password"`
}

func (u *Control) InitUserRoute(r chi.Router) {
	r.Get("/info", app.JsonHttp(nil, u.userInfo))
	r.Post("/modify_password", app.JsonHttp(func() interface{} {
		return &ModifyPwd{}
	}, u.updatePwd))
}

func (u *Control) userInfo(w http.ResponseWriter, r *http.Request, i interface{}) (interface{}, error) {
	token, err := ReadToken(r)
	if err == nil {
		var role Role
		err = u.DB.Db.Model(u.model.Role).First(&role, token.Rid).Error
		if err != nil {
			return nil, TokenError
		}
		return &CurrentUser{
			Name:        token.Name,
			Userid:      token.Uid,
			NotifyCount: 10,
			UnreadCount: 20,
			Access:      role.Value,
		}, nil
	}
	return nil, TokenError
}

func ReadToken(r *http.Request) (*Token, error) {
	token, _ := r.Context().Value(jwtauth.TokenCtxKey).(jwt.Token)
	if usr, ok := token.Get("usr"); ok {
		str := usr.(string)
		arr := strings.Split(str, ",")
		if len(arr) != 3 {
			return nil, TokenError
		}
		var token Token
		userId, err := strconv.ParseUint(arr[0], 10, 64)
		if err != nil {
			return nil, TokenError
		}
		roleId, err := strconv.ParseUint(arr[1], 10, 64)
		if err != nil {
			return nil, TokenError
		}
		token.Uid = uint(userId)
		token.Rid = uint(roleId)
		token.Name = arr[2]
		return &token, nil
	}

	return nil, TokenError
}

func (u *Control) updatePwd(_ http.ResponseWriter, r *http.Request, i interface{}) (rs interface{}, err error) {
	token, err := ReadToken(r)
	if err == nil {
		var user User
		if u.DB.Db.Model(u.model.User).First(&user, token.Uid).Error != nil {
			return nil, UserNotFound
		}
		p := i.(*ModifyPwd)

		if user.Password != "" && !CheckPasswordHash(p.OldPassword, user.Password) {
			return nil, UserPwdError
		}
		p.Password, err = HashPassword(p.Password)
		if err == nil {
			err = u.DB.Db.Model(&user).Update("password", p.Password).Error
		}
	}

	return nil, err
}
